
package com.android.pc.ioc.verification;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.os.AsyncTask;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.android.pc.ioc.verification.annotation.Checked;
import com.android.pc.ioc.verification.annotation.ConfirmPassword;
import com.android.pc.ioc.verification.annotation.Email;
import com.android.pc.ioc.verification.annotation.IpAddress;
import com.android.pc.ioc.verification.annotation.NumberRule;
import com.android.pc.ioc.verification.annotation.Password;
import com.android.pc.ioc.verification.annotation.Regex;
import com.android.pc.ioc.verification.annotation.Required;
import com.android.pc.ioc.verification.annotation.TextRule;

/**
 * A processor that checks all the {@link Rule}s against their {@link View}s.
 * 
 * @author Ragunath Jawahar <rj@mobsandgeeks.com>
 */
public class Validator {
	// Debug
	static final String TAG = Validator.class.getSimpleName();
	static final boolean DEBUG = false;

	private Object mController;
	private boolean mAnnotationsProcessed;
	private List<ViewRulePair> mViewsAndRules;
	private Map<String, Object> mProperties;
	private AsyncTask<Void, Void, ViewRulePair> mAsyncValidationTask;
	private ValidationListener mValidationListener;

	/**
	 * Private constructor. Cannot be instantiated.
	 */
	private Validator() {
		mAnnotationsProcessed = false;
		mViewsAndRules = new ArrayList<Validator.ViewRulePair>();
		mProperties = new HashMap<String, Object>();
	}

	/**
	 * Creates a new {@link Validator}.
	 * 
	 * @param controller
	 *            The instance that holds references to the Views that are being validated. Usually an {@code Activity} or a {@code Fragment}. Also accepts controller instances that have annotated {@code View} references.
	 */
	public Validator(Object controller) {
		this();
		if (controller == null) {
			throw new IllegalArgumentException("'controller' cannot be null");
		}
		mController = controller;
	}

	/**
	 * Interface definition for a callback to be invoked when {@code validate()} is called.
	 */
	public interface ValidationListener {

		/**
		 * Called when all the {@link Rule}s added to this Validator are valid.
		 */
		public void onValidationSucceeded();

		/**
		 * Called if any of the {@link Rule}s fail.
		 * 
		 * @param failedView
		 *            The {@link View} that did not pass validation.
		 * @param failedRule
		 *            The failed {@link Rule} associated with the {@link View}.
		 */
		public void onValidationFailed(View failedView, Rule<?> failedRule);
	}

	/**
	 * Add a {@link View} and it's associated {@link Rule} to the Validator.
	 * 
	 * @param view
	 *            The {@link View} to be validated.
	 * @param rule
	 *            The {@link Rule} associated with the view.
	 * 
	 * @throws IllegalArgumentException
	 *             If {@code rule} is {@code null}.
	 */
	public void put(View view, Rule<?> rule) {
		if (rule == null) {
			throw new IllegalArgumentException("'rule' cannot be null");
		}

		mViewsAndRules.add(new ViewRulePair(view, rule));
	}

	/**
	 * Convenience method for adding multiple {@link Rule}s for a single {@link View}.
	 * 
	 * @param view
	 *            The {@link View} to be validated.
	 * @param rules
	 *            {@link List} of {@link Rule}s associated with the view.
	 * 
	 * @throws IllegalArgumentException
	 *             If {@code rules} is {@code null}.
	 */
	public void put(View view, List<Rule<?>> rules) {
		if (rules == null) {
			throw new IllegalArgumentException("\'rules\' cannot be null");
		}

		for (Rule<?> rule : rules) {
			put(view, rule);
		}
	}

	/**
	 * Convenience method for adding just {@link Rule}s to the Validator.
	 * 
	 * @param rule
	 *            A {@link Rule}, usually composite or custom.
	 */
	public void put(Rule<?> rule) {
		put(null, rule);
	}

	/**
	 * Validate all the {@link Rule}s against their {@link View}s.
	 * 
	 * @throws IllegalStateException
	 *             If a {@link ValidationListener} is not registered.
	 */
	public synchronized void validate() {
		if (mValidationListener == null) {
			throw new IllegalStateException("Set a " + ValidationListener.class.getSimpleName() + " before attempting to validate.");
		}

		ViewRulePair failedViewRulePair = validateAllRules();
		if (failedViewRulePair == null) {
			mValidationListener.onValidationSucceeded();
		} else {
			mValidationListener.onValidationFailed(failedViewRulePair.view, failedViewRulePair.rule);
		}
	}

	/**
	 * Asynchronously validates all the {@link Rule}s against their {@link View}s. Subsequent calls to this method will cancel any pending asynchronous validations and start a new one.
	 * 
	 * @throws IllegalStateException
	 *             If a {@link ValidationListener} is not registered.
	 */
	public void validateAsync() {
		if (mValidationListener == null) {
			throw new IllegalStateException("Set a " + ValidationListener.class.getSimpleName() + " before attempting to validate.");
		}

		// Cancel the existing task
		if (mAsyncValidationTask != null) {
			mAsyncValidationTask.cancel(true);
			mAsyncValidationTask = null;
		}

		// Start a new one ;)
		mAsyncValidationTask = new AsyncTask<Void, Void, ViewRulePair>() {

			@Override
			protected ViewRulePair doInBackground(Void... params) {
				return validateAllRules();
			}

			@Override
			protected void onPostExecute(ViewRulePair pair) {
				if (pair == null) {
					mValidationListener.onValidationSucceeded();
				} else {
					mValidationListener.onValidationFailed(pair.view, pair.rule);
				}

				mAsyncValidationTask = null;
			}

			@Override
			protected void onCancelled() {
				mAsyncValidationTask = null;
			}
		};

		mAsyncValidationTask.execute((Void[]) null);
	}

	/**
	 * Used to find if the asynchronous validation task is running, useful only when you run the Validator in asynchronous mode using the {@code validateAsync} method.
	 * 
	 * @return True if the asynchronous task is running, false otherwise.
	 */
	public boolean isValidating() {
		return mAsyncValidationTask != null && mAsyncValidationTask.getStatus() != AsyncTask.Status.FINISHED;
	}

	/**
	 * Cancels the asynchronous validation task if running, useful only when you run the Validator in asynchronous mode using the {@code validateAsync} method.
	 * 
	 * @return True if the asynchronous task was cancelled.
	 */
	public boolean cancelAsync() {
		boolean cancelled = false;
		if (mAsyncValidationTask != null) {
			cancelled = mAsyncValidationTask.cancel(true);
			mAsyncValidationTask = null;
		}

		return cancelled;
	}

	/**
	 * Returns the callback registered for this Validator.
	 * 
	 * @return The callback, or null if one is not registered.
	 */
	public ValidationListener getValidationListener() {
		return mValidationListener;
	}

	/**
	 * Register a callback to be invoked when {@code validate()} is called.
	 * 
	 * @param validationListener
	 *            The callback that will run.
	 */
	public void setValidationListener(ValidationListener validationListener) {
		this.mValidationListener = validationListener;
	}

	/**
	 * Updates a property value if it exists, else creates a new one.
	 * 
	 * @param name
	 *            The property name.
	 * @param value
	 *            Value of the property.
	 * 
	 * @throws IllegalArgumentException
	 *             If {@code name} is {@code null}.
	 */
	public void setProperty(String name, Object value) {
		if (name == null) {
			throw new IllegalArgumentException("\'name\' cannot be null");
		}

		mProperties.put(name, value);
	}

	/**
	 * Retrieves the value of the given property.
	 * 
	 * @param name
	 *            The property name.
	 * 
	 * @throws IllegalArgumentException
	 *             If {@code name} is {@code null}.
	 * 
	 * @return Value of the property or {@code null} if the property does not exist.
	 */
	public Object getProperty(String name) {
		if (name == null) {
			throw new IllegalArgumentException("\'name\' cannot be null");
		}

		return mProperties.get(name);
	}

	/**
	 * Removes the property from this Validator.
	 * 
	 * @param name
	 *            The property name.
	 * 
	 * @return The value of the removed property or {@code null} if the property was not found.
	 */
	public Object removeProperty(String name) {
		return name != null ? mProperties.remove(name) : null;
	}

	/**
	 * Checks if the specified property exists in this Validator.
	 * 
	 * @param name
	 *            The property name.
	 * 
	 * @return True if the property exists.
	 */
	public boolean containsProperty(String name) {
		return name != null ? mProperties.containsKey(name) : false;
	}

	/**
	 * Removes all properties from this Validator.
	 */
	public void removeAllProperties() {
		mProperties.clear();
	}

	/**
	 * Removes all the rules for the matching {@link View}
	 * 
	 * @param view
	 *            The {@code View} whose rules must be removed.
	 */
	public void removeRulesFor(View view) {
		if (view == null) {
			throw new IllegalArgumentException("'view' cannot be null");
		}

		int index = 0;
		while (index < mViewsAndRules.size()) {
			ViewRulePair pair = mViewsAndRules.get(index);
			if (pair.view == view) {
				mViewsAndRules.remove(index);
				continue;
			}

			index++;
		}
	}

	/**
	 * Validates all rules added to this Validator.
	 * 
	 * @return {@code null} if all {@code Rule}s are valid, else returns the failed {@code ViewRulePair}.
	 */
	private ViewRulePair validateAllRules() {
		if (!mAnnotationsProcessed) {
			createRulesFromAnnotations(getSaripaarAnnotatedFields());
			mAnnotationsProcessed = true;
		}

		if (mViewsAndRules.size() == 0) {
			Log.i(TAG, "No rules found. Passing validation by default.");
			return null;
		}

		ViewRulePair failedViewRulePair = null;
		for (ViewRulePair pair : mViewsAndRules) {
			if (pair == null)
				continue;

			// Validate views only if they are visible and enabled
			if (pair.view != null) {
				if (!pair.view.isShown() || !pair.view.isEnabled())
					continue;
			}

			if (!pair.rule.isValid(pair.view)) {
				failedViewRulePair = pair;
				break;
			}
		}

		return failedViewRulePair;
	}

	private void createRulesFromAnnotations(List<AnnotationFieldPair> annotationFieldPairs) {
		TextView passwordTextView = null;
		TextView confirmPasswordTextView = null;

		for (AnnotationFieldPair pair : annotationFieldPairs) {
			// Password
			if (pair.annotation.annotationType().equals(Password.class)) {
				if (passwordTextView == null) {
					passwordTextView = (TextView) getView(pair.field);
				} else {
					throw new IllegalStateException("You cannot annotate " + "two fields in the same Activity with @Password.");
				}
			}

			// Confirm password
			if (pair.annotation.annotationType().equals(ConfirmPassword.class)) {
				if (passwordTextView == null) {
					throw new IllegalStateException("A @Password annotated field is required " + "before you can use @ConfirmPassword.");
				} else if (confirmPasswordTextView != null) {
					throw new IllegalStateException("You cannot annotate " + "two fields in the same Activity with @ConfirmPassword.");
				} else if (confirmPasswordTextView == null) {
					confirmPasswordTextView = (TextView) getView(pair.field);
				}
			}

			// Others
			ViewRulePair viewRulePair = null;
			if (pair.annotation.annotationType().equals(ConfirmPassword.class)) {
				viewRulePair = getViewAndRule(pair.field, pair.annotation, passwordTextView);
			} else {
				viewRulePair = getViewAndRule(pair.field, pair.annotation);
			}
			if (viewRulePair != null) {
				if (DEBUG) {
					Log.d(TAG, String.format("Added @%s rule for %s.", pair.annotation.annotationType().getSimpleName(), pair.field.getName()));
				}
				mViewsAndRules.add(viewRulePair);
			}
		}
	}

	private ViewRulePair getViewAndRule(Field field, Annotation annotation, Object... params) {
		View view = getView(field);
		if (view == null) {
			Log.w(TAG, String.format("Your %s - %s is null. Please check your field assignment(s).", field.getType().getSimpleName(), field.getName()));
			return null;
		}

		Rule<?> rule = null;
		if (params != null && params.length > 0) {
			rule = AnnotationToRuleConverter.getRule(field, view, annotation, params);
		} else {
			rule = AnnotationToRuleConverter.getRule(field, view, annotation);
		}

		return rule != null ? new ViewRulePair(view, rule) : null;
	}

	private View getView(Field field) {
		try {
			field.setAccessible(true);
			Object instance = mController;

			return (View) field.get(instance);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return null;
	}

	private List<AnnotationFieldPair> getSaripaarAnnotatedFields() {
		List<AnnotationFieldPair> annotationFieldPairs = new ArrayList<AnnotationFieldPair>();
		List<Field> fieldsWithAnnotations = getViewFieldsWithAnnotations();

		for (Field field : fieldsWithAnnotations) {
			Annotation[] annotations = field.getAnnotations();
			for (Annotation annotation : annotations) {
				if (isSaripaarAnnotation(annotation)) {
					annotationFieldPairs.add(new AnnotationFieldPair(annotation, field));
				}
			}
		}

		Collections.sort(annotationFieldPairs, new AnnotationFieldPairCompartor());

		return annotationFieldPairs;
	}

	private List<Field> getViewFieldsWithAnnotations() {
		List<Field> fieldsWithAnnotations = new ArrayList<Field>();
		List<Field> viewFields = getAllViewFields();
		for (Field field : viewFields) {
			Annotation[] annotations = field.getAnnotations();
			if (annotations == null || annotations.length == 0) {
				continue;
			}
			fieldsWithAnnotations.add(field);
		}
		return fieldsWithAnnotations;
	}

	private List<Field> getAllViewFields() {
		List<Field> viewFields = new ArrayList<Field>();

		// Declared fields
		Class<?> superClass = null;
		if (mController != null) {
			viewFields.addAll(getDeclaredViewFields(mController.getClass()));
			superClass = mController.getClass().getSuperclass();
		}

		// Inherited fields
		while (superClass != null && !superClass.equals(Object.class)) {
			List<Field> declaredViewFields = getDeclaredViewFields(superClass);
			if (declaredViewFields.size() > 0) {
				viewFields.addAll(declaredViewFields);
			}
			superClass = superClass.getSuperclass();
		}

		return viewFields;
	}

	private List<Field> getDeclaredViewFields(Class<?> clazz) {
		List<Field> viewFields = new ArrayList<Field>();
		Field[] declaredFields = clazz.getDeclaredFields();
		for (Field f : declaredFields) {
			if (View.class.isAssignableFrom(f.getType())) {
				viewFields.add(f);
			}
		}
		return viewFields;
	}

	private boolean isSaripaarAnnotation(Annotation annotation) {
		Class<?> annotationType = annotation.annotationType();
		return annotationType.equals(Checked.class) || annotationType.equals(ConfirmPassword.class) || annotationType.equals(Email.class) || annotationType.equals(IpAddress.class) || annotationType.equals(NumberRule.class) || annotationType.equals(Password.class) || annotationType.equals(Regex.class) || annotationType.equals(Required.class) || annotationType.equals(TextRule.class);
	}

	private class ViewRulePair {
		public View view;
		public Rule rule;

		public ViewRulePair(View view, Rule<?> rule) {
			this.view = view;
			this.rule = rule;
		}
	}

	private class AnnotationFieldPair {
		public Annotation annotation;
		public Field field;

		public AnnotationFieldPair(Annotation annotation, Field field) {
			this.annotation = annotation;
			this.field = field;
		}
	}

	private class AnnotationFieldPairCompartor implements Comparator<AnnotationFieldPair> {

		@Override
		public int compare(AnnotationFieldPair lhs, AnnotationFieldPair rhs) {
			int lhsOrder = getAnnotationOrder(lhs.annotation);
			int rhsOrder = getAnnotationOrder(rhs.annotation);
			return lhsOrder < rhsOrder ? -1 : lhsOrder == rhsOrder ? 0 : 1;
		}

		private int getAnnotationOrder(Annotation annotation) {
			Class<?> annotatedClass = annotation.annotationType();
			if (annotatedClass.equals(Checked.class)) {
				return ((Checked) annotation).order();
			} else if (annotatedClass.equals(ConfirmPassword.class)) {
				return ((ConfirmPassword) annotation).order();
			} else if (annotatedClass.equals(Email.class)) {
				return ((Email) annotation).order();
			} else if (annotatedClass.equals(IpAddress.class)) {
				return ((IpAddress) annotation).order();
			} else if (annotatedClass.equals(NumberRule.class)) {
				return ((NumberRule) annotation).order();
			} else if (annotatedClass.equals(Password.class)) {
				return ((Password) annotation).order();
			} else if (annotatedClass.equals(Regex.class)) {
				return ((Regex) annotation).order();
			} else if (annotatedClass.equals(Required.class)) {
				return ((Required) annotation).order();
			} else if (annotatedClass.equals(TextRule.class)) {
				return ((TextRule) annotation).order();
			} else {
				throw new IllegalArgumentException(String.format("%s is not a Saripaar annotation", annotatedClass.getName()));
			}
		}
	}

}
